package com.y.report.service.api;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONValidator;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.y.report.cons.ApiConstant;
import com.y.report.entity.api.ApiRequestInfo;
import com.y.report.entity.api.ApiResponseInfo;
import com.y.report.entity.basic.YReportApi;
import com.y.report.entity.basic.YReportSql;
import com.y.report.entity.mapper.ApiReportInfo;
import com.y.report.exception.ApiExecption;
import com.y.report.util.HumpUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
public class ApiArgsService {

    private static Logger log = LoggerFactory.getLogger(ApiArgsService.class);

    @Autowired
    private HttpServletRequest request;

    private static final String TOKEN_PATTERN = "\\{\\w+-?\\w*}";

    /**
     * params:regexp
     * params:data,template
     * reutrn :cvtData,list
     */
    private static final BiFunction<String, Pair<Object, String>, Pair<String, Map<String, Object>>> PARSE = (String p, Pair<Object, String> tpl) -> {
        Map<String, Object> coll = new LinkedHashMap<>();
        String rtData = tpl.getValue();
        Pattern pattern = Pattern.compile(p);
        Matcher m = pattern.matcher(rtData);
        while (m.find()) {
            String k = m.group().replaceAll(".?\\{|}.?", "");
            int start = m.start();
            int end = m.end();
            String v = null;

            Object origin = tpl.getKey();
            boolean isObj = false;
            if (origin instanceof String) {
                String startStr = tpl.getValue().substring(0, start);
                String endStr = tpl.getValue().substring(end);
                v = StrUtil.subBetween(tpl.getKey().toString(), startStr, endStr);
            } else if (origin instanceof Map) {
                Optional<Map.Entry<String, Object>> vOpt = ((Map<String, Object>) origin)
                        .entrySet()
                        .stream()
                        .filter(x -> k.equals(x.getKey()))
                        .findFirst();
                if (vOpt.isPresent())
                    if (Objects.nonNull(vOpt.get().getValue()))
                        if (vOpt.get().getValue() instanceof String)
                            v = vOpt.get().getValue().toString();
                        else {
                            v = JSON.toJSONString(vOpt.get().getValue(), SerializerFeature.WriteMapNullValue);
                            isObj = true;
                        }
                    else v = null;
            }
            final String tokenK = "__replace_key_RDS_CHAR_DOLLAR";
            if (StrUtil.isNotBlank(v))
                v = v.replaceAll("\\$", tokenK);

            if (isObj)
                rtData = rtData.replaceFirst(String.format("\"%s\"", p), v);
            else
                rtData = m.replaceFirst(v == null ? "" : v);

            rtData = rtData.replaceAll(tokenK, "\\$");
            m.reset(rtData);
            coll.put(k, v);
        }

        try {
            //repair
            rtData = rtData.replaceAll("\"\\{", "{").replaceAll("}\"", "}").replaceAll("\"\\[", "[").replaceAll("]\"", "}");
            rtData = JSON.toJSONString(JSON.parse(rtData, Feature.AllowUnQuotedFieldNames, Feature.AllowSingleQuotes, Feature.AllowArbitraryCommas, Feature.IgnoreNotMatch, Feature.OrderedField));
        } catch (Exception e) {
            try {
                rtData = rtData.replaceAll("\\\\\"", "\"");
                rtData = JSON.toJSONString(JSON.parse(rtData, Feature.AllowUnQuotedFieldNames, Feature.AllowSingleQuotes, Feature.AllowArbitraryCommas, Feature.IgnoreNotMatch, Feature.OrderedField));
            } catch (Exception exception) {
                log.warn(e.getMessage());
            }
        }

        if (JSONValidator.from(rtData).validate()) {
            JSONObject dictObj = JSON.parseObject(rtData);
            if (CollUtil.isNotEmpty(dictObj))
                rtData = JSON.toJSONString(
                        dictObj.entrySet().stream().map(x -> {
                            Object v = x.getValue();
                            if (v != null) {
                                JSONValidator jvd = JSONValidator.from(v.toString());
                                if (jvd.validate()) {
                                    switch (jvd.getType()) {
                                        case Array:
                                            v = JSON.parseArray(v.toString());
                                            break;
                                        case Object:
                                            v = JSON.parseObject(v.toString());
                                            break;
                                    }
                                }
                            }
                            return Pair.of(x.getKey(), v);
                        }).collect(LinkedHashMap::new, (x1, x2) -> x1.put(x2.getKey(), x2.getValue()), Map::putAll),
                        SerializerFeature.WriteMapNullValue);
        }

        return Pair.of(rtData, coll);
    };

    public void parseApiArgsInfo(ApiRequestInfo apiRequestInfo) throws IOException {
        apiRequestInfo.setUrlArgs(urlArgs());
        apiRequestInfo.setHeaderArgs(headerArgs());
        apiRequestInfo.setBodyArgs(bodyArgs(apiRequestInfo.getBody(), x -> foreachParseArgs(x, x)));
        apiRequestInfo.setCookieArgs(cookieArgs());
    }

    public void parseTemplateApiArgsInfo(ApiRequestInfo apiRequestInfo, ApiReportInfo apiReportInfo) {
        YReportApi yRrportApi = apiReportInfo.getYReportApi();

        Map<String, Object> urlArgs = apiRequestInfo.getUrlArgs();
        apiRequestInfo.setUrlArgs(mergeArgs(templateArgs(JSON.toJSONString(urlArgs, SerializerFeature.WriteMapNullValue), yRrportApi.getApiUrl()), urlArgs, ApiConstant.API_URL_REPEAT_ARGS_PREFIX));

        Map<String, Object> headerArgs = apiRequestInfo.getHeaderArgs();
        apiRequestInfo.setHeaderArgs(mergeArgs(templateArgs(JSON.toJSONString(headerArgs, SerializerFeature.WriteMapNullValue), yRrportApi.getApiHeaders()), headerArgs, ApiConstant.API_HEADER_REPEAT_ARGS_PREFIX));

        Map<String, Object> bodyArgs = apiRequestInfo.getBodyArgs();
        apiRequestInfo.setBodyArgs(mergeArgs(templateArgs(JSON.toJSONString(bodyArgs, SerializerFeature.WriteMapNullValue), yRrportApi.getReqTemplateBody()), bodyArgs, ApiConstant.API_BODY_REPEAT_ARGS_PREFIX));

        Map<String, Object> cookieArgs = apiRequestInfo.getCookieArgs();
        apiRequestInfo.setCookieArgs(mergeArgs(templateArgs(JSON.toJSONString(cookieArgs, SerializerFeature.WriteMapNullValue), yRrportApi.getApiCookies()), cookieArgs, ApiConstant.API_COOKIE_REPEAT_ARGS_PREFIX));
    }

    public void parseSqlTemplate(Map<String, Object> sqlArgs, ApiReportInfo apiReportInfo) {
        apiReportInfo.getYSqlInfos()
                .forEach(x -> {
                    YReportSql yRrportSql = x.getYReportSql();
                    yRrportSql.setSqlContent(PARSE.apply("(?<![\\$#])" + TOKEN_PATTERN, Pair.of(sqlArgs, yRrportSql.getSqlContent())).getKey());
                });
    }

    public Map<String, Object> parseRespBodyTemplate(Map<String, Object> sqlArgs, ApiResponseInfo apiResponseInfo) {
        Map<String, Object> responseMapping = JSON.parseObject(JSON.toJSONString(apiResponseInfo, SerializerFeature.WriteMapNullValue))
                .entrySet()
                .stream()
                .map(x -> Pair.of(ApiConstant.API_RESPONSE_TEMPLATE_PREFIX + HumpUtil.toUnderlineCase(x.getKey()), x.getValue()))
                .collect(LinkedHashMap::new, (x1, x2) -> x1.put(x2.getKey(), x2.getValue()), Map::putAll);

        Map<String, Object> respBodyTemplate = apiResponseInfo.getRespBodyTemplate();
        //simple responseBody template
        if (CollUtil.isEmpty(respBodyTemplate)) {
            respBodyTemplate = MapUtil.<String, Object>builder()
                    .put("elapsed", String.format("${%s%s}", ApiConstant.API_RESPONSE_TEMPLATE_PREFIX, "elapsed"))
                    .put("error_message", String.format("${%s%s}", ApiConstant.API_RESPONSE_TEMPLATE_PREFIX, "error_message"))
                    .put("message", String.format("${%s%s}", ApiConstant.API_RESPONSE_TEMPLATE_PREFIX, "message"))
                    .put("success", String.format("${%s%s}", ApiConstant.API_RESPONSE_TEMPLATE_PREFIX, "return_flag"))
                    .put("data", String.format("${%s%s}", ApiConstant.API_RESPONSE_TEMPLATE_PREFIX, "output_data"))
                    .build();
        }

        Pair<Object, String> pair = Pair.of(mergeArgs(sqlArgs, responseMapping, null), JSON.toJSONString(respBodyTemplate, SerializerFeature.WriteMapNullValue));

        Pair<String, Map<String, Object>> fmt;
        try {
            fmt = PARSE.apply("\\$" + TOKEN_PATTERN, pair);
            JSONObject result = JSON.parseObject(fmt.getKey());
        } catch (Exception e) {
            return Collections.singletonMap("error_message", "解析结果出错，请检查sql输出结果！");
        }
        return JSON.parseObject(fmt.getKey());
    }

    public Map<String, Object> mergeArgs(Map<String, Object> src, Map<String, Object> dest, String prefix) {
        if (src != null && dest != null) {
            Map<String, Object> rtMap = src.entrySet()
                    .stream()
                    .map(x -> {
                        String k = x.getKey();
                        if (StringUtils.isNotBlank(prefix) && dest.containsKey(k))
                            k = prefix + k;
                        return Pair.of(k, x.getValue());
                    }).collect(LinkedHashMap::new, (x, y) -> x.put(y.getKey(), y.getValue()), Map::putAll);

            rtMap.putAll(dest);
            return rtMap;
        } else if (MapUtil.isNotEmpty(src)) return src;
        else if (MapUtil.isNotEmpty(dest)) return dest;
        else return MapUtil.empty();
    }


    public boolean parseApiShowLog(Map<String, Object> sqlArgs) {
        return MapUtil.isNotEmpty(sqlArgs) && sqlArgs.containsKey(ApiConstant.API_RESPONSE_SHOW_API_LOG_KEY) &&
                sqlArgs.getOrDefault(ApiConstant.API_RESPONSE_SHOW_API_LOG_KEY, "false").toString().trim().equalsIgnoreCase("true");
    }

    public Map<String, Object> parseRespHeaders(Map<String, Object> respArgs, ApiReportInfo apiReportInfo) {
        Map<String, Object> rtHeaderMap = new LinkedHashMap<>();
        String respTemplateHeaders = apiReportInfo.getYReportApi().getRespTemplateHeaders();
        if (StringUtils.isNotBlank(respTemplateHeaders)) {
            if (!JSONValidator.Type.Value.equals(JSONValidator.from(respTemplateHeaders).getType())) {
                JSONObject jsonObject = JSON.parseObject(PARSE.apply("\\$" + TOKEN_PATTERN, Pair.of(respArgs, respTemplateHeaders)).getKey());
                jsonObject.forEach(rtHeaderMap::put);
            } else
                log.warn("api响应头格式错误，解析失败！  ->{}", respTemplateHeaders);
        }

        return rtHeaderMap;
    }

    public Map<String, Object> parseRespCookies(Map<String, Object> respArgs, ApiReportInfo apiReportInfo) {
        Map<String, Object> rtHeaderMap = new LinkedHashMap<>();
        String respTemplateCookies = apiReportInfo.getYReportApi().getRespTemplateCookies();
        if (StringUtils.isNotBlank(respTemplateCookies)) {
            if (!JSONValidator.Type.Value.equals(JSONValidator.from(respTemplateCookies).getType())) {
                JSONObject jsonObject = JSON.parseObject(PARSE.apply("\\$" + TOKEN_PATTERN, Pair.of(respArgs, respTemplateCookies)).getKey());
                jsonObject.forEach(rtHeaderMap::put);
            } else
                log.warn("api响应cookie格式错误，解析失败！  ->{}", respTemplateCookies);
        }

        return rtHeaderMap;
    }

    public Map<String, Object> parseRespBody(Map<String, Object> respArgs, ApiReportInfo apiReportInfo) {
        final Map<String, Object> rtBodyMap = new LinkedHashMap<>();
        String respTemplateBody = apiReportInfo.getYReportApi().getRespTemplateBody();
        if (StringUtils.isNotBlank(respTemplateBody)) {
            if (!JSONValidator.Type.Value.equals(JSONValidator.from(respTemplateBody).getType())) {
                JSONObject jsonObject = JSON.parseObject(PARSE.apply("\\$" + TOKEN_PATTERN, Pair.of(respArgs, respTemplateBody)).getKey());
                jsonObject.forEach((k, v) -> {
                    if (StrUtil.isEmpty(k) || ObjectUtil.isEmpty(v) || (v instanceof String && StrUtil.isEmpty((String) v))) {
                        if (JSONValidator.Type.Object.equals(JSONValidator.from(respTemplateBody).getType()))
                            v = JSON.parseObject(respTemplateBody).get(k);
                    }
                    rtBodyMap.put(k, v);
                });
            } else
                log.warn("api响应体格式错误，解析失败！  ->{}", respTemplateBody);
        }
        return rtBodyMap;
    }


    private Map<String, Object> templateArgs(String origin, String template) {
        Map<String, Object> argsMap = new LinkedHashMap<>();
        if (StrUtil.isNotBlank(origin) && StrUtil.isNotEmpty(template)) {
            origin = origin.replaceAll(" ", "");
            template = template.replaceAll(" ", "");

            Pair<String, Map<String, Object>> tpl = PARSE.apply("(?<![\\$#])" + TOKEN_PATTERN, Pair.of(origin, template));
            //argsMap.putAll(tpl.getValue());

            tpl = PARSE.apply("\\$" + TOKEN_PATTERN, Pair.of(origin, tpl.getKey()));
            argsMap.putAll(tpl.getValue());

            tpl = PARSE.apply("#" + TOKEN_PATTERN, Pair.of(origin, tpl.getKey()));
            argsMap.putAll(tpl.getValue());

        }
        return argsMap;
    }


    private Map<String, Object> urlArgs() {
        return request.getParameterMap()
                .entrySet()
                .stream()
                .map(x -> {
                    String[] values = x.getValue();
                    AtomicInteger index = new AtomicInteger(0);
                    Map<String, Object> rtMap = MapUtil.<String, Object>of(x.getKey(), ArrayUtil.isEmpty(values) ? null : values[0]);
                    if (ArrayUtil.isNotEmpty(values) && values.length > 1)
                        rtMap = Arrays.stream(x.getValue())
                                .map(y -> MapUtil.of(String.format("%s[%d]", x.getKey(), index.getAndIncrement()), y))
                                .flatMap(y -> y.entrySet().stream())
                                .<Map<String, Object>>collect(LinkedHashMap::new, (x1, x2) -> x1.put(x2.getKey(), x2.getValue()), Map::putAll);
                    return rtMap;
                })
                .flatMap(x -> x.entrySet().stream())
                .<Map<String, Object>>collect(LinkedHashMap::new, (x1, x2) -> x1.put(x2.getKey(), x2.getValue()), Map::putAll);
    }

    public Map<String, Object> headerArgs() {
        Map<String, Object> outputHeaderMap = new LinkedHashMap<>();
        for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements(); ) {
            String headerName = headerNames.nextElement();
            for (Enumeration<String> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements(); )
                outputHeaderMap.put(headerName, headerValues.nextElement());
        }
        return outputHeaderMap;
    }

    public <OUT> OUT bodyArgs(String body, Function<String, OUT> format) throws IOException {
        if (body != null) return format.apply(body);
        try (InputStream is = request.getInputStream()) {
            StringBuilder sb = new StringBuilder();
            byte[] b = new byte[1024];
            for (int n; (n = is.read(b)) != -1; )
                sb.append(new String(b, 0, n, StandardCharsets.UTF_8));

            return format.apply(sb.toString());
        } catch (IOException e) {
            throw e;
        }
    }

    public Map<String, Object> cookieArgs() throws IOException {
        if (request.getCookies() == null) return Collections.emptyMap();
        return Arrays.stream(request.getCookies())
                .map(x -> Pair.of(x.getName(), x.getValue()))
                .collect(LinkedHashMap::new, (x1, x2) -> x1.put(x2.getKey(), x2.getValue()), Map::putAll);
    }

    public Map<String, Object> foreachParseArgs(String k, String in) {
        Map<String, Object> out = new LinkedHashMap<>();
        if (StrUtil.isNotBlank(in) && JSONValidator.from(in).validate()) {
            try {
                Object parseObj = JSON.parse(in);
                JSONArray jsonArray = new JSONArray();
                if (parseObj instanceof JSONArray) {
                    jsonArray = (JSONArray) parseObj;
                } else if (parseObj instanceof JSONObject) {
                    jsonArray.add(parseObj);
                } else {
                    jsonArray.add(JSONObject.parse(String.format("{\"%s\":\"%s\"}", k, StrUtil.splitTrim(in, "\""))));
                }
                jsonArray.forEach(obj -> {
                    if (obj instanceof JSONObject) {
                        ((JSONObject) obj).forEach((key, value) -> {
                            if (ObjectUtil.isNotEmpty(value) && !JSONValidator.Type.Value.equals(JSONValidator.from(JSON.toJSONString(value, SerializerFeature.WriteMapNullValue)).getType())) {
                                out.putAll(foreachParseArgs(key, JSON.toJSONString(value, SerializerFeature.WriteMapNullValue)));
                            } else
                                out.put(key, value);
                        });
                    } else {
                        out.putAll(foreachParseArgs(k, JSON.toJSONString(obj, SerializerFeature.WriteMapNullValue)));
                    }
                });
            } catch (Exception e) {
                throw e;
            }

        }
        return out;
    }


    public boolean verify(ApiRequestInfo apiRequestInfo, ApiReportInfo apiReportInfo) {
        final YReportApi yReportApi = apiReportInfo.getYReportApi();
        if (StrUtil.isNotBlank(yReportApi.getReqTemplateBody()))
            try {
                if (!bodyArgs(apiRequestInfo.getBody(), (x -> {
                    apiRequestInfo.setBody(x);
                    if (StrUtil.isNotBlank(x)) {
                        final String inputStr = x.replaceAll("\\s|\\t|\\r", "");
                        final String templateStr = yReportApi.getReqTemplateBody().replaceAll("\\s|\\t|\\r", "");
                        final String[] tokens = templateStr.split("\\$" + TOKEN_PATTERN);
                        return Arrays.stream(tokens).allMatch(inputStr::contains);
                    }
                    return true;
                }))) {
                    apiReportInfo.getYReportApi().setRespStatus("402");
                    throw new ApiExecption("输入参数错误，请检查输入参数格式！");
                }
            } catch (IOException e) {
                log.warn(e.getMessage());
                apiReportInfo.getYReportApi().setRespStatus("402");
                throw new ApiExecption("输入参数格式错误！ err:" + e.getMessage());
            }

        return true;
    }
}
